package api

import (
	"context"
	"errors"
	"fmt"
	"grpcPrimary/pb"
	"io"
	"log"
	"strings"

	"github.com/gofrs/uuid"
	epb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

type Server struct {
	productMap map[string]*pb.Product
	orderMap   map[string]*pb.Order
}

func (s *Server) InitTest() {
	s.orderMap = make(map[string]*pb.Order)
	s.orderMap["1"] = &pb.Order{Items: []string{"test", "test1"}, Destination: "test1"}
	s.orderMap["2"] = &pb.Order{Items: []string{"test", "test1"}, Destination: "test2"}
	s.orderMap["3"] = &pb.Order{Items: []string{"test", "test1"}, Destination: "test1"}
	s.orderMap["4"] = &pb.Order{Items: []string{"test", "test1"}, Destination: "test3"}

}

func (s *Server) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductId, error) {
	out, err := uuid.NewV4()
	if err != nil {
		return nil, status.Errorf(codes.Internal, "Error while generating Product Id", err)
	}
	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		log.Printf("md: %v", md)
	}
	in.Id = out.String()
	if s.productMap == nil {
		s.productMap = make(map[string]*pb.Product)
	}
	s.productMap[in.Id] = in

	//
	header := metadata.Pairs("header-key", "val")
	grpc.SendHeader(ctx, header)
	//
	trailer := metadata.Pairs("trailer-key", "val")
	grpc.SetTrailer(ctx, trailer)

	return &pb.ProductId{Value: in.Id}, status.New(codes.OK, "").Err()
}

func (s *Server) GetProduct(ctx context.Context, in *pb.ProductId) (*pb.Product, error) {
	value, exists := s.productMap[in.Value]
	if exists {
		return value, status.New(codes.OK, "").Err()
	}
	return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}

func (s *Server) GetOrder(ctx context.Context, in *wrapperspb.StringValue) (*pb.Order, error) {
	value, exists := s.orderMap[in.Value]
	if exists {
		return value, status.New(codes.OK, "").Err()
	}

	errStatus := status.New(codes.NotFound, "Order does not exist")
	ds, err := errStatus.WithDetails(
		&epb.BadRequest_FieldViolation{
			Field: "ID",
			Description: fmt.Sprintf("Order ID receive is not valid: %s",
				in.Value),
		},
	)
	if err != nil {
		return nil, errStatus.Err()
	}
	return nil, ds.Err()
}

func (s *Server) SearchOrders(searchQuery *wrapperspb.StringValue, stream pb.OrderManagement_SearchOrdersServer) error {
	for key, order := range s.orderMap {
		log.Print(key, order)
		for _, itemStr := range order.Items {
			log.Print(itemStr)
			if strings.Contains(itemStr, searchQuery.Value) {
				err := stream.Send(order)
				if err != nil {
					return fmt.Errorf("error sending message to stream : %v", err)
				}
				log.Print("Matching Order Found : " + key)
				break
			}
		}
	}
	return nil
}

func (s *Server) UpdateOrders(stream pb.OrderManagement_UpdateOrdersServer) error {
	ordersStr := "Updated Order IDs : "
	for {
		order, err := stream.Recv()
		if err == io.EOF {
			return stream.SendAndClose(&wrapperspb.StringValue{Value: "Orders processed " + ordersStr})
		}
		s.orderMap[order.Id] = order
		log.Print("Order ID ", order.Id, ": Updated")
		ordersStr += order.Id + ", "
	}
}

func (s *Server) ProcessOrders(stream pb.OrderManagement_ProcessOrdersServer) error {
	combinedShipmentMap := make(map[string]*pb.CombinedShipment)
	batchMarker := 0
	const orderBatchSize = 10
	header := metadata.Pairs("header-key", "val")
	stream.SendHeader(header)
	trailer := metadata.Pairs("trailer-key", "val")
	stream.SetTrailer(trailer)
	for {
		orderId, err := stream.Recv()
		if err == io.EOF {
			for _, comb := range combinedShipmentMap {
				stream.Send(comb)
			}
			return nil
		}

		md, ok := metadata.FromIncomingContext(stream.Context())
		if ok {
			log.Printf("md: %v", md)
		}

		if err != nil {
			return err
		}
		//handle
		order, exist := s.orderMap[orderId.Value]
		if !exist {
			return errors.New("No such order" + order.Id)
		}

		if value, exits := combinedShipmentMap[order.Destination]; exits {
			value.OrderList = append(value.OrderList, order)
		} else {
			combinedShip := &pb.CombinedShipment{OrderList: make([]*pb.Order, 0)}
			combinedShip.OrderList = append(combinedShip.OrderList, order)
			combinedShipmentMap[order.Destination] = combinedShip
		}

		if batchMarker == orderBatchSize {
			for _, comb := range combinedShipmentMap {
				stream.Send(comb)
			}
			batchMarker = 0
			combinedShipmentMap = make(map[string]*pb.CombinedShipment)
		} else {
			batchMarker++
		}
	}
}
